#if defined(UNIT_TEST)
#include "unit_test.h"
#else

#include "apr.h"
#include "apr_strings.h"
#include "apr_thread_proc.h"
#include "apr_hash.h"
#include "apr_user.h"
#include "apr_lib.h"
#include "apr_optional.h"
#endif

#include "mod_atom.h"

/* one global regex to match /\.atom$/ */
static ap_regex_t * dot_atom;

/* Iterate over all the entries in a collection, in reverse chronological
 *  order.
 */

typedef struct {
    apr_pool_t * pool;
    apr_hash_t * seen;
    char * base;
    char * * y;
    int y_ind;
    char * * m;
    int m_ind;
    char * * d;
    int d_ind;
    char * * entries;
    int e_ind;
} atom_iter_t;

typedef enum {
    YYYY, MM, DD, ENTRY
} level_t;

typedef struct {
    char * filename;
    apr_time_t mtime;
} filetime_t;

static int compare_yyyymmdd(const void * s1_p, const void * s2_p) {
    char * s1 = *((char * *) s1_p);
    char * s2 = *((char * *) s2_p);
    return strcmp((char *) s2, (char *) s1);
}

static int compare_entries(const void * f1_p, const void * f2_p) {
    filetime_t * f1 = (filetime_t *) f1_p;
    filetime_t * f2 = (filetime_t *) f2_p;

    if (f1->mtime > f2->mtime)
        return -1;
    else if (f1->mtime < f2->mtime)
        return 1;
    else
        return 0;
}

/* Read a directory, starting at one of YYYY, MM, DD, or Entries level,
 *  and fill in the levels below appropriately.
 * Inclusion and sorting semantics differ between levels.
 */
static char * refresh(atom_iter_t * iter, level_t ymd) {
    apr_array_header_t * a = apr_array_make(iter->pool, 32, sizeof (char *));
    apr_dir_t * dir;
    apr_finfo_t finfo;
    char * * at;
    char * dirname = NULL;

    switch (ymd) {
    case YYYY:
        dirname = iter->base;
        iter->y_ind = 0;
        break;
    case MM:
        dirname = atom_join(iter->pool, iter->base, iter->y[iter->y_ind], NULL);
        iter->m_ind = 0;
        break;
    case DD:
        dirname = atom_join(iter->pool, iter->base,
                            iter->y[iter->y_ind], iter->m[iter->m_ind], NULL);
        iter->d_ind = 0;
        break;
    case ENTRY:
        dirname = atom_join(iter->pool, iter->base,
                            iter->y[iter->y_ind], iter->m[iter->m_ind],
                            iter->d[iter->d_ind], NULL);
        iter->e_ind = 0;
        break;
    }

    if (apr_dir_open(&dir, dirname, iter->pool) == APR_SUCCESS) {
        while (apr_dir_read(&finfo, APR_FINFO_NAME | APR_FINFO_TYPE, dir) ==
                APR_SUCCESS) {
            char * this;

            if (finfo.name[0] == '.')
                continue;
            if (ymd == ENTRY) {
                /* at the entry level, only regular files */
                if (finfo.filetype != APR_REG)
                    continue;

                /* links as a result of PUT updates */
                if (strncmp(finfo.name, LINK_PREFIX, strlen(LINK_PREFIX)) == 0) {

                    /* read the link */
                    char * linkname = atom_join(iter->pool, dirname, finfo.name, NULL);
                    atom_snarf(linkname, &this, iter->pool);

                    /* have we already seen it? */
                    if (apr_hash_get(iter->seen, this, APR_HASH_KEY_STRING)) {
                        apr_file_remove(linkname, iter->pool);
                        continue;
                    }

                    /* is it still there? */
                    if (!atom_file_exists(this, iter->pool)) {
                        apr_file_remove(linkname, iter->pool);
                        continue;
                    }
                } else {
		    /* we only want files that end in ".atom" */
		    if (MATCHER(dot_atom, finfo.name) != 0)
                        continue;

                    this = atom_join(iter->pool, dirname, finfo.name, NULL);

                    /* might have seen it already */
                    if (apr_hash_get(iter->seen, this, APR_HASH_KEY_STRING))
                        continue;
                }
            } else {
                /* at the yyyymmdd level, only directories with numeric names
                 *  of the appropriate lengths
                 */
                int i;
                if (finfo.filetype != APR_DIR)
                    continue;
                for (i = 0; finfo.name[i]; i++)
                    if (!apr_isdigit(finfo.name[i]))
                        break;
                if (finfo.name[i] != 0)
                    continue;
                if (ymd == YYYY) {
                    if (i != 4)
                        continue;
                } else if (i != 2)
                    continue;
            }

            at = (char * *) apr_array_push(a);
            if (ymd == ENTRY) {
                /* remember that we've seen this */
                apr_hash_set(iter->seen, this, APR_HASH_KEY_STRING, "!");

                *at = this;
            } else
                *at = apr_pstrdup(iter->pool, finfo.name);
        }
        apr_dir_close(dir);

        if (ymd == ENTRY) {

            /* these gyrations because there's no qsort_r on many systems, and
             *  you need access to the pool to do an apr_stat, so you'd have to
             *  put that in a global which would be a no-no in multi-threaded mode,
             *  so we get all the filetimes right here & now and make a little
             *  list of name/time pairs which are self-contained & can be sorted.
             */
            int i;
            filetime_t * ft =
                    (filetime_t *) apr_palloc(iter->pool, a->nelts * sizeof (filetime_t));
            char * * names = (char * *) a->elts;

            for (i = 0; i < a->nelts; i++) {
                apr_finfo_t finfo;
                ft[i].filename = names[i];
                apr_stat(&finfo, names[i], APR_FINFO_MTIME, iter->pool);
                ft[i].mtime = finfo.mtime;
            }

            qsort(ft, a->nelts, sizeof (filetime_t), compare_entries);

            for (i = 0; i < a->nelts; i++)
                names[i] = ft[i].filename;
        } else
            qsort(a->elts, a->nelts, sizeof (char *), compare_yyyymmdd);

        at = (char * *) apr_array_push(a);
        *at = NULL;
    }
    switch (ymd) {
    case YYYY:
        iter->y = (char * *) a->elts;
        if (iter->y[0])
            return refresh(iter, MM);
        else {
            /* signal empty collection */
            iter->entries = iter->m = iter->d = iter->y;
            return NULL;
        }
    case MM:
        iter->m = (char * *) a->elts;
        return refresh(iter, DD);
    case DD:
        iter->d = (char * *) a->elts;
        return refresh(iter, ENTRY);
    case ENTRY:
        iter->entries = (char * *) a->elts;
        return NULL;
    }
    return NULL;
}

void atom_iter_init(apr_pool_t * pool) {
    dot_atom = ap_pregcomp(pool, "\\.atom$", AP_REG_EXTENDED);
}

char * atom_iter_new(apr_pool_t * pool, char * dir, void * * iter_p) {
    char * problem;
    atom_iter_t * iter =
            (atom_iter_t *) apr_pcalloc(pool, sizeof (atom_iter_t));

    iter->pool = pool;
    iter->base = apr_pstrdup(pool, dir);
    iter->seen = apr_hash_make(pool);
    if ((problem = refresh(iter, YYYY)))
        return problem;
    *iter_p = iter;
    return NULL;
}

char * atom_iter_next(void * iter_a, char * * next_p) {
    atom_iter_t * iter = (atom_iter_t *) iter_a;
    char * problem;

    if (iter->entries[iter->e_ind] != NULL) {
        *next_p = iter->entries[iter->e_ind++];
        return NULL;
    }

    iter->d_ind++;
    if (iter->d[iter->d_ind] != NULL) {
        if ((problem = refresh(iter, ENTRY)))
            return problem;
        return atom_iter_next(iter_a, next_p);
    }

    iter->m_ind++;
    if (iter->m[iter->m_ind] != NULL) {
        if ((problem = refresh(iter, DD)))
            return problem;
        return atom_iter_next(iter_a, next_p);
    }

    iter->y_ind++;
    if (iter->y[iter->y_ind] != NULL) {
        if ((problem = refresh(iter, MM)))
            return problem;
        return atom_iter_next(iter_a, next_p);
    }

    *next_p = NULL;
    return NULL;
}
